/*
 * Copyright (C) 2013 SeekDeepWithin.com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.seekdeepwithin.DataAccess;

import com.seekdeepwithin.Data.*;
import com.seekdeepwithin.MainController;
import com.sun.jersey.api.client.*;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import java.util.*;
import javafx.scene.control.*;
import javax.ws.rs.core.*;
import org.json.*;

/**
 * User: Jonathan Montiverdi
 * Date: Sep 19, 2013
 *
 * This class will connect to seek deep within.com and access the REST API.
 */
public class SdwApi
{
   private static SdwApi s_Instance;
   private Client m_Client;
   private WebResource m_Service;
   private String m_ApiVersion;
   private String m_DbVersion;

   /**
    * Private constructor to prevent improper use.
    */
   private SdwApi ()
   {
      ClientConfig config = new DefaultClientConfig ();
      this.m_Client = Client.create (config);
      this.m_Service = this.m_Client.resource (UriBuilder.fromUri ("http://seekdeepwithin.com/code/api.php").build ());
   }

   /**
    * Gets the seek deep with API instance.
    * @return The one and only instance of the seek deep within API.
    */
   public static SdwApi getInstance ()
   {
      if (s_Instance == null) {
         s_Instance = new SdwApi ();
      }
      return s_Instance;
   }

   /**
    * Gets the version of the API on the seek deep within server.
    * @return The server's API version.
    */
   public String getApiVersion ()
   {
      if (this.m_ApiVersion == null || this.m_ApiVersion.isEmpty ()) {
         this.m_ApiVersion = this.m_Service.queryParam ("action", "getapiversion")
                 .accept (MediaType.TEXT_PLAIN).get (String.class);
      }
      return this.m_ApiVersion;
   }

   /**
    * Gets the version of the database on the seek deep within server.
    * @return The server's database version.
    */
   public String getDatabaseVersion ()
   {
      if (this.m_DbVersion == null || this.m_DbVersion.isEmpty ()) {
         this.m_DbVersion = this.m_Service.queryParam ("action", "getdbversion")
                 .accept (MediaType.TEXT_PLAIN).get (String.class);
      }
      return this.m_DbVersion;
   }

   /**
    * Gets the list of all the areas.
    * @return Areas list - Map of 'AreaID' and 'AreaName';
    */
   public Map<Integer, String> getAllAreas ()
   {
      Map<Integer, String> data = new HashMap<> ();

      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getallareas")
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));
      Iterator areaList = json.keys ();
      while (areaList.hasNext ()) {
         String areaId = areaList.next ().toString ();
         data.put (Integer.parseInt (areaId), json.getString (areaId));
      }

      return data;
   }

   /**
    * Gets the list of all the style types.
    * @return Style type list - Map of 'StyleTypeId' and 'StyleTypeName';
    */
   public Map<Integer, String> getAllStyleTypes ()
   {
      Map<Integer, String> data = new HashMap<> ();

      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getallstyletypes")
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));
      Iterator styleTypeList = json.keys ();
      while (styleTypeList.hasNext ()) {
         String styleTypeId = styleTypeList.next ().toString ();
         data.put (Integer.parseInt (styleTypeId), json.getString (styleTypeId));
      }

      return data;
   }

   /**
    * Gets the list of all the styles on the server.
    * @return List of styles.
    */
   public List<Style> getAllStyles ()
   {
      List<Style> styles = new ArrayList<> ();
      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getallstyles")
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));
      Iterator styleList = json.keys ();
      while (styleList.hasNext ()) {
         String styleId = styleList.next ().toString ();
         JSONObject styleItem = json.getJSONObject (styleId);
         styles.add (new Style (Integer.parseInt (styleId), styleItem.getInt ("typeid"), styleItem.getString ("style")));
      }
      return styles;
   }

   /**
    * Gets the list of bookList on the seek deep within server.
    */
   public ArrayList<Book> getBookList ()
   {
      ArrayList<Book> books = new ArrayList<> ();
      try {
         JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "listbooks")
                 .accept (MediaType.APPLICATION_JSON)
                 .get (String.class));
         Iterator bookList = json.keys ();
         while (bookList.hasNext ()) {
            String bookItem = bookList.next ().toString ();
            JSONObject bookData = json.getJSONObject (bookItem);
            books.add (this.buildBook (bookItem, bookData));
         }
      } catch (UniformInterfaceException exception) {
         Dialogs.showErrorDialog (MainController.getInstance ().getStage (),
                                  "Unable to download library book information from the server! Please try again later.",
                                  "An error occured while downloading the library from the server.", "Error", exception);
      }

      return books;
   }

   /**
    * Gets the book data for the book with the given ID.
    * @param title Title of book to get sub books for.
    * @return A book object with the data downloaded from the server.
    */
   public Book getBookData (String title)
   {
      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getbookdata")
              .queryParam ("book", title)
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));
      return buildBook (title, json);
   }

   /**
    * Builds a book object from the given information.
    * @param title Title of book.
    * @param json Book's data in JSON format.
    * @return A book object.
    * @throws NumberFormatException
    * @throws JSONException
    */
   private Book buildBook (String title, JSONObject json) throws NumberFormatException, JSONException
   {
      Book book = new Book (title, json.getInt ("id"));
      book.setAuthor (json.getString ("author"));
      book.setInfo (json.getString ("info"));
      book.setDate (json.getString ("date"));
      book.setSubTitle (json.optString ("subtitle"));
      book.setAbbreviation (json.getString ("abbreviation"));
      book.setHasSubBooks (json.getInt ("hassubbooks") == 1);
      book.setHasChapters (json.getInt ("haschapters") == 1);
      book.setAmazonId (json.getString ("amazonid"));
      book.setIsSubBook (json.getInt ("issubbook") == 1);
      // Sources
      JSONArray sources = json.optJSONArray ("sources");
      if (sources != null) {
         int length = sources.length ();
         for (int i = 0; i < length; i++) {
            JSONObject sourceData = sources.getJSONObject (i);
            book.addSource (sourceData.getInt ("sourceid"), sourceData.getString ("source"),
                            sourceData.getInt ("nameid"), sourceData.getString ("name"));
         }
      }
      // Translators
      JSONObject translators = json.optJSONObject ("translators");
      if (translators != null) {
         Iterator translatorList = translators.keys ();
         while (translatorList.hasNext ()) {
            String translatorId = translatorList.next ().toString ();
            String translator = translators.getString (translatorId);
            book.addTranslator (Integer.parseInt (translatorId), translator);
         }
      }
      return book;
   }

   /**
    * Gets a list of sub books for the given book.
    * @param parentBook Book to get sub books for.
    */
   public void getSubBooks (Book parentBook)
   {
      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getsubbooklist")
              .queryParam ("bookid", String.valueOf (parentBook.getId ()))
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));

      Iterator subBookList = json.keys ();
      while (subBookList.hasNext ()) {
         String subBookListId = subBookList.next ().toString ();
         JSONObject subBookItem = json.getJSONObject (subBookListId);
         String subBookTitle = subBookItem.getString ("title");
         //int subBookId = subBookItem.getInt ("subbookid");
         int subBookOrder = subBookItem.getInt ("order");
         int subBookLinkedId = subBookItem.optInt ("linkedbooklistid", -1);
         Book b = this.getBookData (subBookTitle);
         SubBook subBook = new SubBook (b, Integer.parseInt (subBookListId), parentBook);
         subBook.setOrder (subBookOrder);
         if (subBookLinkedId != -1) {
            subBook.setLinkedId (subBookLinkedId);
         }
         parentBook.getSubBooks ().add (subBook);
      }
   }

   /**
    * Gets the list of chapters for the given sub book.
    * @param subBook Sub book to get chapters for.
    */
   public void getChapters (SubBook subBook)
   {
      int linkId = subBook.getLinkedId ();
      Integer subBookId = linkId == -1 ? subBook.getSubBookListId () : linkId;
      JSONObject json = new JSONObject (this.m_Service.queryParam ("action", "getchapterlist")
              .queryParam ("subbookid", subBookId.toString ())
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));

      Iterator chapterList = json.keys ();
      while (chapterList.hasNext ()) {
         String chapterListId = chapterList.next ().toString ();
         JSONObject chapterItem = json.getJSONObject (chapterListId);
         String chapterName = chapterItem.getString ("name");
         int chapterId = chapterItem.getInt ("chapterid");
         int chapterOrder = chapterItem.getInt ("order");
         JSONObject footer = chapterItem.optJSONObject ("footer");
         JSONObject header = chapterItem.optJSONObject ("header");
         Chapter chapter = new Chapter (chapterId, chapterName, Integer.parseInt (chapterListId));
         chapter.setOrder (chapterOrder);
         if (footer != null) {
            chapter.setFooter (footer.getInt ("id"), footer.getString ("footer"));
         }
         if (header != null) {
            chapter.setHeader (header.getInt ("id"), header.getString ("header"));
         }
         subBook.getChapters ().add (chapter);
      }
   }

   /**
    * Gets the passages for the given chapter from the server.
    * @param chapter Chapter to get passages for.
    */
   public void getPassages (Chapter chapter)
   {
      Integer chapterListId = chapter.getChapterListId ();
      JSONArray json = new JSONArray (this.m_Service.queryParam ("action", "getpassages")
              .queryParam ("chapterid", chapterListId.toString ())
              .accept (MediaType.APPLICATION_JSON)
              .get (String.class));
      int passageCount = json.length ();
      for (int i = 0; i < passageCount; i++) {
         JSONObject passageItem = json.getJSONObject (i);
         Passage passage = new Passage (passageItem.getInt ("passageid"),
                                        passageItem.getString ("passage"),
                                        passageItem.getInt ("passagenumber"),
                                        passageItem.getInt ("order"));
         JSONObject header = passageItem.optJSONObject ("header");
         if (header != null) {
            passage.setHeader (header.getInt ("id"), header.getString ("text"));
         }
         JSONArray footers = passageItem.optJSONArray ("footers");
         if (footers != null) {
            int footerCount = footers.length ();
            for (int j = 0; j < footerCount; j++) {
               JSONObject footerItem = footers.getJSONObject (j);
               passage.addFooter (footerItem.getInt ("id"), footerItem.getString ("footer"), footerItem.getInt ("index"));
            }
         }
         JSONObject links = passageItem.optJSONObject ("links");
         if (links != null) {
            for (Iterator it = links.keys (); it.hasNext ();) {
               String linkId = it.next ().toString ();
               JSONObject linkItem = links.getJSONObject (linkId);
               passage.addLink (Integer.parseInt (linkId), linkItem.getInt ("start"),
                                linkItem.getInt ("end"), linkItem.getString ("area"),
                                linkItem.getString ("link"));
            }
         }
         JSONObject definitions = passageItem.optJSONObject ("definitions");
         if (definitions != null) {
            for (Iterator it = definitions.keys (); it.hasNext ();) {
               String defid = it.next ().toString ();
               JSONObject defItem = definitions.getJSONObject (defid);
               passage.addDefinition (Integer.parseInt (defid), defItem.getInt ("start"),
                                      defItem.getInt ("end"), defItem.getString ("item"));
            }
         }
         JSONObject styles = passageItem.optJSONObject ("styles");
         if (styles != null) {
            for (Iterator it = styles.keys (); it.hasNext ();) {
               String passageStyleId = it.next ().toString ();
               JSONObject styleItem = styles.getJSONObject (passageStyleId);
               passage.addStyle (Integer.parseInt (passageStyleId), styleItem.getInt ("start"),
                                 styleItem.getInt ("end"), styleItem.getInt ("styleid"),
                                 styleItem.getInt ("styletypeid"), styleItem.optString ("style"));
            }
         }
         chapter.getPassages ().add (passage);
      }
   }
}
